在看雪高研班课时⑦中进行SSL库的内存漫游的时候,发现疑似可以dump所有证书的点,也就是org.conscrypt.ActiveSession的实例域peerCertificates。
可以以soul为例,尝试开发dump其客户端证书的通杀脚本。http://aosp.opersys.com/xref/android-8.1.0_r1/xref/external/conscrypt/common/src/main/java/org/conscrypt/ActiveSession.java#50先是搜索ConscryptFileDescriptorSocket类对象,找到ActiveSession类对象。
在ActiveSession类对象中找到peerCertificates变量。这里的peerCertificates以数组形式存放了多个证书。之前课程中对soul进行过分析,这里直接取出asset中的p12证书文件,放入KeyStore Explorer中查看基本信息来作为参照。
观察多个不同session中peerCertificates的值,发现没有符合的证书。
然后注意到ActiveSession类对象中除了peerCertificates还有一个localCertificates。peerCertificates应该是服务器端的证书,localCertificates是本地app的证书。
导出的证书已经可以被KeyStore Explorer正确识别了,但是无法导入到Charles中。原因在于这个导出的证书中不包含私钥信息,所以还需要找到证书对应的私钥内容或者找到包含私钥的完整证书。Android中的Https通信开发中基本上都会需要定义一个SSLContext类对象来配置SSL的一些关键参数。配置时需要调用其init初始化方法。
需要传入一个KeyManager数组,和一个TrustManager数组。TrustManager是用来验证服务器端证书的,比如经典的checkServerTrusted。KeyManager数组是包含了客户端(app)本地的证书(包括私钥),用来传给服务器端来对客户端进行验证的。那么我们当前的目标涉及的就是找到KeyManager对象中的本地证书(私钥)。回到前面包含ActiveSession类对象的ConscryptFileDescriptorSocket实例,看看Socket对象中是否包含其中包含相关信息。找到一个SSLParametersImpl类对象。
这个sslParameters中包含了一个X509KeyManager类对象和一个X509TrustManager类对象,似乎就是我们需要的东西。
另外,从源码中SSLContext.init方法追溯KeyManager传递过程,可以发现其传给了一个contextSpi对象。然后在该对象的engineInit方法中会创建一个SSLParametersImpl类对象,并用KeyManager和TrustManager等内容将其初始化。这样就可以和刚才的发现连起来了。说明理论上来说,SSLParametersImpl确实有可能存有我关心的本地证书相关信息。用Wallbreaker可以看出这个KeyManager对象的具体实现类是KeyManagerImpl。这个类的实例对象可以调用getPrivateKey获取到证书私钥。
而别名可以通过KeyManagerImpl对象的chooseXXAlias系列方法获取。这里要获取客户端上的证书,应该调用chooseClientAlias方法。这个方法需要传入三个参数,第一个是加密算法名称的列表,第二个是证书发行方的列表,第三个是所属的socket(可以为null)。获取到alias再去调用getPrivateKey方法应该就可以获取到对应的私钥了。ConscryptFileDescriptorSocket类中的getSSLParameter方法返回的是一个SSLParameters类对象,而不是我需要的SSLParametersImpl类对象。
在前面获取证书的脚本中添加额外代码。先获取SSLParametersImpl类对象:
这里有个坑是不要用类提供的getSSLParameters方法,返回的内容和想要的不是同一个东西。源码中查看这个getSSLParameters方法,返回了一个SSLParameters类对象。这个类也不能强制转化成我想要的SSLParametersImpl类,不知道是个什么东西。然后按着之前看的流程实现一遍,一直到拿到KeyManagerImpl类对象。这里也是只有包含了localCertificates的socket我们才感兴趣。这里打印了KeyManagerImpl的hash变量值,因为Wallbreaker中看到这个值就是获取所要信息的关键。Hook完运行后,和预计的一样得到了hash的值。可以发现,这里不仅包含一个Private key entry,也包含了一个证书链。这个证书链中只有一项,就是那份本地证书。按正常获取PrivateKey的步骤,应该根据密钥加密算法列表和证书发布者列表去获取目标对象的alias(hash值)。
但是尝试后得到的alias始终为null,暂时未找到原因。考虑到大部分情况下本地存放的用于传给服务器校验的证书应该只有一份(不一定准确),暂且用遍历的方式取出hashTable中的所有内容。这里打印出了hashTable值所属的实际类为PrivateKeyEntry:调用其getPrivateKey方法就可以得到与私钥相关的对象:而调用getCertificate方法可以获取证书链最末端的一份证书。这里研究的目标只有一份证书,所以返回的就是目标证书。这里的getPrivateKey方法返回的是一个PrivateKey接口类型,需要定位其具体的实现类并用Java.cast转一下后才能调用类的getEncoded方法输出私钥内容。这里我没找到什么好办法,最后是直接通过Wallbreaker查看的具体实现类。但不同的app可能会用不同的类来实现这个PrivateKey接口,所以是个有待优化的点。最后这部分实现大概是这样,将证书和私钥同时写入文件:
把两个生成的文件pull到本地,放入KeyStore Explorer中分别查看,可以识别。为了导入Charles,需要看下Charles支持的证书导入格式。有两种支持的格式,PEM格式和P12格式。一般P12格式的证书文件中包含了证书和公私钥密钥对,文件内容是二进制编码的。而PEM格式的证书文件可能不包含私钥,如果需要的话私钥可以单独存放在一个文件中,文件内容是base64编码的。可以看到Charles中导入PEM证书时,需要分别选择填入PEM编码的私钥和PEM编码的证书。
而刚才在KeyStore Explorer中,可以将hook出来的文件分别转成PEM编码格式。
虽然初步完成了任务,但每次需要把证书和私钥分开导出来并分别转成PEM格式,再放入Charles显然很麻烦。既然现在可以同时hook到证书和其对应的私钥,理论上应该可以直接导出一个同时包含证书和私钥的P12格式文件,然后直接导入Charles使用。这里搜索到了一篇相关的博客介绍如何以开发视角完成这个操作:
https://blog.csdn.net/u011077027/article/details/100847057
这里用到的参数都是刚才hook出来的内容,那直接依样画葫芦就可以了。把导出的文件直接导入Charles(P12格式):
至此,感觉过程中还是略过了不少细节。这些细节的忽略可能让这次的脚本遇到其他app时达不到效果。不过有了这个基本的框架,自认为对大部分应用来说,遇到后都可以见招拆招了。
看雪ID:Avacci
https://bbs.pediy.com/user-home-879855.htm
*本文由看雪论坛 Avacci 原创,转载请注明来自看雪社区